Miyamasuアプデしてた ~GUIなしPlayerモードで非同期テスト最高~
概要
そのうち「2017 Unity 春の自作テストツール祭り」みたいなのをやるので、それに出品(?)する。
非同期テストも書けるリモートテストランナー兼、なにかになった。 ver 1.3.0。
そんな Miyamasu
https://github.com/sassembla/Miyamasu
アプデ内容
非同期テストが書けるUnity用のテストツール、みたいなの、みなさん自作してると思うんだ。
Miyamasuもそんな中の一つなんだけど、今回はいろんな実行モードを追加していた。
・Editor実行(既存)
・Player on Editor実行(改良)
・Player実行(新規、調整中)
・Batch Player実行(新規)
・CloudBuild実行(新規、頓挫中)
このうち、CloudBuild実行は、Miyamasu のユニットテストを搭載したプロジェクトをUnity CloudBuildにアップした際、
その上でテストが走る、みたいなモードなんだけど、
とにかくNUnitの使い方が残念なのと、Unityが非同期テストを軽んじてて辛いのでまだ実現できていない。
Editor実行(既存)
コンパイルが走るたびに自動的に実行できる。
ただし、PurchaseとAssetBundle周りの非同期コードが一部Playerでしか進捗しないため、それらのテストだけはできない。
人間が任意でtestをSkipするみたいな機構を入れることで回避した。
Player on Editor実行(改良)
これはそのまま、Play時にMiyamasuのテストケースが実行されるようになった。
いろいろ手が込んだことをした結果、通常のアプリケーションを走らせたままテストが可能になったと思う。
Player実行(新規、調整中)
書かれたテストを適当に実機とかでも実行できる。
もうちょい調整する。リモート実行、リモートレポートとかをガッツリ入れてて、面白いことになってる。
Batch Player実行(新規)
バッチ実行で、ヘッドレスのPlayerが起動するようにした。
ようはUnity Editor上でのPlayer状態でのTest実行を、batchから起動することが可能になった。
例えばAutoyaのリポジトリでこれを使ってて、sh run_miyamasu_tests.sh とかやると、UnityがUIなしで起動した状態で、Playerモードになった上でテストが走る。
課金とかAssetBundleとかの「非同期を含んでで、MainThreadでしか動かせない」系のテストも無事突破した。これってすごくない?
結構面白かった。
CloudBuild実行(新規、頓挫中)
具体的に言うと、NUnitのユニットテストでは、main threadを自由に回しつつ、main threadで他の処理を走らせつつ、非同期処理を待つ、ということができない。
これは、たとえUnityにasync/await とかが来ても、使い物にならないということを言おうとしてる。
代替策としてMiyamasu作ったんだけど、思ったよりも事態は辛い感じだった。
Unityの構造が~とか、NUnitが~~とかそういうのではなく、NUnitの使い方がダメ、みたいな話。
NUnitのユニットテストを起動すると、次のような特徴のある動きをする。
・テストはMainThreadで起動(PlayerモードでもEditorモードでもない、そのほかのモード
・テスト中、Instantiateとかをやっても問題ないので、PlayerのMainThreadか、EditorApplication.updateの属するスレッド(EditorMainThread)に近い特性を持つ。
・なおかつユニットテスト中、EditorApplication.updateは完全停止している(これはほんとに困った)
・例えば別スレッドで行われる非同期な処理など、特定の処理の終了を待つ方法がない(MainThreadなので、これをロックすると全てが停止する)
・じゃあ起動だけでもできればいいか、、と思って新規にスレッドを立ち上げると、そのスレッドからMainThreadに対してActionとかを放り込むことができないため、MainThread系のメソッドのテストができない
この文章読んだだけだとしっくりこないかもしれないが、
「非同期テストをするためには、非同期処理の完了を待たなければいけない」という常識的な操作に対して、NUnitの起動スレッドが、停止を許されないMainThreadであることが、致命的になっている。 たとえばawaitとかで「ほかのthreadが終わるのを待つ」という処理を書くと、もれなくUnityEditor全体が停止する。これだとTestが進まない。
ちなみにNUnitのテストでの SynchronizationContext.Current はnullなので、何もできない気がする。Playingの時かつMainThreadでのみ値が入って動くみたいだ。
なおかつ、UnityにはMainThreadでしかできない処理があるので、その処理を放り込む先 = MainThreadが停止してる ということがあるせいで、ろくなテストが書けない。
理想形を考える
理想形はどんななんだろう、って考えると、
・擬似メインスレッドで起動しつつ、実際のPlayerスレッドとは違って「メインスレッドをロックせずに特定の処理が終わるのをメインスレッドで待つ」という処理ができないといけない。
いっそ特殊なPlay状態にでもしちゃって、特定処理が帰って来たら処理復帰する、それまでは空回りする、
みたいなIEnumerator返す関数みたいな処理がテストユニットとして動作できるといいのになあと思う。
ようは、NUnitを使うにしても、IEnumeratorを返すようなテストユニットに改変しちゃって、MainThreadからユニットテスト自体をMoveNextさせられればいいんじゃないか。
まあMiyamasuがそれなんだけど。公式でもっとしっかりしたのがあると嬉しい。
適当なコードにするとしたらこんな感じ。
public class CloudBuildTestEntryPoint {
[Test] public static IEnumerator RunFromNUnit () {// IEnumeratorを返す、ユニットテスト単位の擬似MainThreadで実行されるNUnitのテスト処理
// 完了待ちをするbool
var done = false;
// 適当なGameObjectを作成
var go = new GameObject("test");
var mb = go.AddComponent<MainThreadRunner>();
// 別スレッドでテストを実行する処理(IEnumeratorを返してくる)。
// mbを取り込んで、MainThreadでやってほしい処理を実行させる。
// ここの書き方はどうでもいい。
RunOnAnotherThread(
() => {
var path = “test:”;
// mbに対してMainThreadで実行して欲しいiEnumeratorを送って、mbのUpdateで実行する = MainThreadに処理を放り込む。
mb.Commit(
() => {
path = path + Application.dataPath;// このメソッドはMainThreadでしか呼べない
}
);
// なんか非同期処理
done = true;
}
);
// 上の処理が終わるまでここで空回りさせる
while (!done) {
yield return null;
}
// なんかAssertとかの処理
}
}
公式でないと作れないのが、「PlayモードでなくてもPlayモードと同じように動く、ユニットテスト単位で後片付けとか上手な機構」という感じで、
こういうのがうまく書けるようになるといいな~~と思う。
何度も言うけどMiyamasuはそれができてるんで、まあ、公式でないのが欠点。